package nl.ceasaro.playingcards;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;

/**
 * Represents a collection of cards, typically comprised of one or more decks.
 *
 * @author Garrett Smith, gsmith at northwestern dot edu
 * @version Blackjack v1.0
 * @since Blackjack v1.0
 */
public class CardContainer
{

   private static final Random _RANDOM = new Random();
   private final ArrayList<Card> _cards;
   private final ArrayList<Card> _discardedCards;
   private boolean _autoReset;
   private Double _resetPercent = null;
   private final int _originalSize;
   private Integer _resetNumber = null;

   /**
    * Creates a shuffled container with its cards set to
    * those of one <code>{@link Deck}</code>.
    *
    * @param deck the deck to place in this container
    */
   public CardContainer(final Deck deck)
   {
      this(deck, 1);
   }

   /**
    * Creates a shuffled container with cards from <code>numDecks</code>
    * number of <code>{@link Deck}</code>.
    *
    * @param deck     the kind of deck to place in this container
    * @param numDecks the amout of deck to add to the container
    */
   public CardContainer(final Deck deck, final int numDecks)
   {
      this(deck, numDecks, true);
   }


   /**
    * Creates a container with cards from <code>numDecks</code>
    * number of <code>decks</code>; will be shuffled according to the value of
    * <code>shuffle</code>.
    *
    * @param deck     the kind of deck to place in this container
    * @param numDecks the amout of deck to add to the container
    * @param shuffle  if true the cards in the container are shuffled
    */
   public CardContainer(final Deck deck, final int numDecks, final boolean shuffle)
   {
      _originalSize = deck.size() * numDecks;
      _cards = new ArrayList<Card>(_originalSize);
      _discardedCards = new ArrayList<Card>(_originalSize);
      for (int i = 0; i < numDecks; i++)
      {
         _cards.addAll(deck.getAllCards());
      }
      if (shuffle)
      {
         shuffle();
      }
   }

   /**
    * Creates an unshuffled container with its cards equal to the contents of
    * <code>cards<code>.
    *
    * @param cards The deck to used to fill this container
    */
   public CardContainer(final List<Card> cards)
   {
      _originalSize = cards.size();
      _cards = new ArrayList<Card>(cards);
      _discardedCards = new ArrayList<Card>(_originalSize);
   }

   /**
    * Returns whether or not auto reset is on.
    *
    * @return <code>true</code> if auto reset is on, <code>false</code> if
    *         not.
    */
   public boolean isAutoResetOn()
   {
      return _autoReset;
   }

   /**
    * Removes and returns a single card from the container.
    * If both auto reset is on and the reset limit is
    * reached then the container will be reset and shuffled.
    *
    * @return a card taken from the top of the container.
    * @throws NoMoreCardsException if there are no more cards in the container and
    *                              auto reset is not on.
    */
   public Card getCard()
   {
      if (isResetShuffleOnAndNeeded())
      {
         resetShuffle();
      }
      if (this.isEmpty())
      {
         throw new NoMoreCardsException("container is out of cards");
      }

      Card card = _cards.get(0);
      _cards.remove(card);
      _discardedCards.add(card);
      return card;
   }

   /**
    * @return all available cards in the container.
    */
   public List<Card> getAllCardsInDeck()
   {
      return _cards;
   }

   /**
    * @return all cards discarded from the container
    */
   public List<Card> getAllDiscardedCardsFromDeck()
   {
      return _discardedCards;
   }

   /**
    * Returns whether this container has any cards left in it.
    *
    * @return <code>true</code> if the container has at least one card in it,
    *         <code>false</code> if not.
    */
   public boolean hasMoreCards()
   {
      return (_cards.size() > 0);
   }

   /**
    * Returns whether this container is empty.
    *
    * @return <code>true</code> if the container is empty, <code>false</code>
    *         if not.
    */
   public boolean isEmpty()
   {
      return !hasMoreCards();
   }

   /**
    * Resets this container with the original number and distribution of cards;
    * does not re-shuffle (randomly re-order) the container and does not return
    * the cards to their original order.
    */
   public void reset()
   {
      _cards.addAll(_discardedCards);
      _discardedCards.clear();
   }

   /**
    * Resets this container with the original number and type distribution of
    * cards and re-shuffles (randomly re-orders) the cards.
    */
   public void resetShuffle()
   {
      reset();
      shuffle();
   }

   /**
    * @return the original size of this container.
    */
   public int originalSize()
   {
      return _originalSize;
   }

   /**
    * Returns the number of undealt cards in the container
    *
    * @return the number of undealt cards
    */
   public int size()
   {
      return _cards.size();
   }

   /**
    * <p/>
    * Turns on auto reset, where the container is automatically reset AND
    * reshuffled (radomly re-ordered) whenever the number of undealt cards
    * falls below <code>percentage</code> percent the original number.
    * </p><p>
    * If this method is invoked after {@link #setAutoReset(int)},
    * auto reset will still be on but will use a percentage threshold rather
    * than threshold based on the absolute number of cards remaining.
    * </p>
    *
    * @param percentage the percent undealt cards left that must be reached
    *                   before the deck is reset and reshuffled.  Must be
    *                   greater than 0 and less than or equal to 1.0.
    */
   public void setAutoReset(double percentage)
   {
      if (percentage <= 0.0 || percentage > 1.0)
      {
         throw new IllegalArgumentException(
                 "percentage must be greater than 0.0 and less "
                         + "than or equal to 1.0: " + percentage);
      }
      _autoReset = true;
      _resetPercent = percentage;
      _resetNumber = null;
   }

   /**
    * <p/>
    * Turns on auto reset, where the container is automatically reset AND
    * reshuffled (radomly re-ordered) whenever the number of undealt cards
    * falls below <code>limit</code>.
    * </p><p>
    * If this method is invoked after {@link #setAutoReset(double)}, auto reset will still be
    * on but will be triggered by the number of cards limit rather than a
    * percentage limit.
    * </p>
    *
    * @param limit the number of undealt cards that the container that must have
    *              less than before the container is reset and reshuffled.
    */
   public void setAutoReset(final int limit)
   {
      if (limit <= 0 || limit > _cards.size() + _discardedCards.size())
      {
         throw new IllegalArgumentException(
                 "limit must be greater than zero and less than the original "
                         + "size of the deck: " + limit);
      }
      _autoReset = true;
      _resetNumber = limit;
      _resetPercent = null;
   }

   /**
    * Turns off auto reset.
    *
    * @see #setAutoReset(int)
    * @see #setAutoReset(double)
    */
   public void setAutoResetOff()
   {
      _autoReset = false;
   }

   /**
    * <p/>
    * Shuffles (randomly orders) the contents of the container.
    * </p><p>
    * This will not reset the container; only the undealt cards will be shuffled.
    * Use {@link #resetShuffle()} to both reset and shuffle the container.
    * </p>
    */
   public void shuffle()
   {
      Collections.shuffle(_cards, CardContainer._RANDOM);
   }

   /**
    * Returns a string representation of this container.
    *
    * @return a string representation of the container.
    */
   public String toString()
   {
      StringBuffer ret = new StringBuffer();
      ret.append("CardContainer=[");
      int stopPoint = _cards.size() - 1;
      for (int i = 0; i < stopPoint; i++)
      {
         ret.append(_cards.get(i).toString()).append(",");
      }
      ret.append(_cards.get(_cards.size() - 1).toString()).append("]");
      return ret.toString();
   }  // end String toString()

   private boolean isResetShuffleOnAndNeeded()
   {
      return _autoReset
              && (isResetShuffleNeededByInt()
              || isResetShuffleNeededByDouble());
   }

   private boolean isResetShuffleNeededByInt()
   {
      return _resetNumber != null
              && _cards.size() <= _resetNumber;
   }

   private boolean isResetShuffleNeededByDouble()
   {
      return _resetPercent != null
              && (_resetPercent * _originalSize)
              > _cards.size();
   }

   /**
    * @param amount the number of cards to be returned.
    * @return a set of cards.
    */
   public Set<Card> getCards(int amount)
   {
      HashSet<Card> cards = new HashSet<Card>();
      for (int i = 0; i < amount; i++)
      {
         cards.add(getCard());
      }
      return cards;
   }
}
